Vue 3 组件间通信的4种方式
Vue 3 中组件间数据传递有以下四种核心方式:
| 方式 | 方向 | API | 适用场景 |
|---|---|---|---|
| Props | 父 -> 子 | defineProps | 数据下传 |
| Emit | 子 -> 父 | defineEmits | 事件上报 |
| 依赖注入 | 跨层级 | provide / inject | 深层嵌套通信 |
| 状态管理 | 全局 | Pinia | 多组件共享状态 |
本节以 Swiper 轮播组件的实战案例,深入讲解 Emit 和 $attrs 的用法。
子到父通信:defineEmits 实战
场景描述
首页的 Swiper 轮播区域采用左右布局:左侧为轮播图,右侧为对应的内容描述。当用户在左侧切换轮播时,右侧内容需要同步更新。
Emit 事件定义
子组件通过 defineEmits 声明可触发的事件:
// SwiperContent.vue
const emits = defineEmits<{
(e: 'change', payload: { activeIndex: number }): void
}>()
function onSlideChange(swiper: SwiperType) {
emits('change', { activeIndex: swiper.activeIndex })
}
typescript
父组件监听与处理
// index.vue
const selectedItem = ref(items[0])
function handleSwiperChange({ activeIndex }: { activeIndex: number }) {
selectedItem.value = items[activeIndex]
}
// 也可使用解构语法简化
// const { activeIndex } = e
// selectedItem.value = items[activeIndex]
typescript
模板绑定
<template>
<div class="flex">
<!-- 左侧轮播 -->
<SwiperContent @change="handleSwiperChange" />
<!-- 右侧内容 -->
<div class="w-1/3">
<h3>{{ selectedItem.title }}</h3>
<p>{{ selectedItem.subtitle }}</p>
<a :href="selectedItem.url" target="_blank">查看更多</a>
</div>
</div>
</template>
vue
$attrs 属性透传
使用场景
当需要复用一个已有组件但传递未在 Props 中定义的属性时,使用 v-bind="$attrs" 进行属性透传。
以合作伙伴区域的 Swiper 为例,复用首页轮播组件但需要覆盖多个默认属性:
<!-- 合作伙伴轮播:复用首页组件,覆盖默认属性 -->
<SwiperContent
:slides-per-view="4"
:height="200"
:pagination="undefined"
:navigation="undefined"
:loop="true"
:space-between="10"
>
<template #default="{ item }">
<img :src="item.image" :alt="item.name" />
</template>
</SwiperContent>
vue
子组件接收 $attrs
<!-- SwiperContent.vue -->
<template>
<Swiper v-bind="$attrs">
<slot />
</Swiper>
</template>
vue
属性覆盖机制
v-bind="$attrs" 的属性会覆盖组件内部 Swiper 的同名默认属性:
外部传入 :slides-per-view="4" --> 覆盖内部默认值 1
外部传入 :height="200" --> 覆盖内部默认值 28rem
外部传入 :pagination="undefined" --> 覆盖内部默认 pagination 配置
text
属性透传的注意事项
1. 必须使用绑定语法
<!-- 正确:绑定语法,$attrs 中可取到 -->
<SwiperContent :pagination-hide="true" />
<!-- 错误:简写语法,不会被解析到 $attrs -->
<SwiperContent pagination-hide />
vue
2. 通过 $attrs 条件渲染控制子组件内容
<!-- 子组件中根据 $attrs 判断是否显示 pagination -->
<template>
<Swiper v-bind="$attrs">
<div v-if="!$attrs.paginationHide">
<!-- pagination 内容 -->
</div>
</Swiper>
</template>
vue
3. 使用空 template 占位隐藏 slot
<!-- 通过空 template 隐藏具名 slot -->
<SwiperContent>
<template #pagination>
<span /> <!-- 空元素占位,隐藏 pagination -->
</template>
</SwiperContent>
vue
4种方式的选择指南
是否是父子关系?
├── 是 --> 数据传递? --> Props
│ └── 事件上报? --> Emit
└── 否 --> 是否跨多层?
├── 是 --> 依赖注入 (provide/inject)
└── 否 --> 多组件共享? --> 状态管理 (Pinia)
text
Props 传参示例
// 父组件
<Card title="前端课程" subtitle="从入门到精通" />
// 子组件
const props = defineProps<{
title: string
subtitle: string
}>()
typescript
Emit 传参示例
// 子组件
const emit = defineEmits<{
(e: 'change', index: number): void
}>()
emit('change', 2)
// 父组件
<ChildComponent @change="handleChange" />
typescript
依赖注入示例
// 祖先组件提供数据
provide('theme', ref('dark'))
// 后代组件注入数据
const theme = inject<Ref<string>>('theme')
typescript
Pinia 状态管理示例
// store/user.ts
export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserInfo | null>(null)
return { userInfo }
})
// 任意组件中使用
const userStore = useUserStore()
console.log(userStore.userInfo)
typescript
要点总结
defineEmits是 Vue 3 子到父通信的标准方式,支持类型化声明v-bind="$attrs"可以透传未定义的 Props,实现属性覆盖- 属性覆盖必须使用
:prop="value"绑定语法 - 组件通信方案的选择取决于组件关系的层级和共享范围
- 复用组件时,
$attrs+ 具名 slot 是灵活控制子组件行为的有效手段
↑